using System;
using System.Collections.Generic;
using System.Data;
using System.Reflection;

namespace ODX.Core
{
    internal static class DataSetBuilder
    {
        internal static DataSet BuildDataSet(Polymorpher pm)
        {
            DataSet ds = new DataSet();
            foreach (string table in pm.GetTables())
            {
                DataTable dt = ds.Tables.Add(table);

                DataColumn id = dt.Columns.Add("ID", typeof (string));
                AddProperties(new ColumnInfo(id.ColumnName, true, 22), id);

                dt.Constraints.Add("PK_" + dt.TableName, id, true);

                if (pm.DoesTableVersioned(table))
                {
                    DataColumn dc = dt.Columns.Add(SpecialColumns.RowVersion, typeof (string));
                    AddProperties(new ColumnInfo(dc.ColumnName, false, 22), dc);
                }
            }

            foreach (string table in pm.GetTables())
            {
                string parent = pm.GetTypeTable(pm.GetRootTypeForTable(table).BaseType);
                if ( parent != null )
                {
                    DataTable baseTable = ds.Tables[parent];
                    DataTable inheritedTable = ds.Tables[table];

                    CreateRelation(ds, baseTable.TableName, inheritedTable.TableName, new ColumnInfo("ID", false, 22));
                }
            }

            foreach ( Type type in pm.GetTypes() )
            {
                string table = pm.GetTypeTable(type);
                if ( table == null )
                    continue;

                DataTable dt = ds.Tables[table];

                if (BuilderUtils.IsHierarchyRoot(type))
                {
                    DataColumn dc = dt.Columns.Add(Session.TypeDefField, typeof (string));
                    AddProperties(new ColumnInfo(dc.ColumnName,false,22), dc);
                }

                foreach (PropertyInfo pi in type.GetProperties(BuilderUtils.PropertyFilter))
                {
                    if ( !BuilderUtils.IsAutoProperty(pi) )
                        continue;

                    if (pi.PropertyType.IsSubclassOf(typeof(Entity)))
                        CreateParent(pi, dt, pm);
                    else if (new List<Type>(pi.PropertyType.GetInterfaces()).Contains(typeof(IEntityListBase)))
                        CreateList(pi, dt, pm);
                    else
                        CreateAtom(pi, dt);
                }
            }

            foreach ( DataTable dt in ds.Tables )
            {
                foreach ( Constraint c in dt.Constraints )
                {
                    // Single-column unique constraints are created by assigning DataColumn.Unique property
                    // So it's much easier to rename constraints. Otherwise they is named "constraint1", ...
                    UniqueConstraint uc = c as UniqueConstraint;
                    if (uc != null && !uc.IsPrimaryKey && uc.Columns.Length == 1)
                        uc.ConstraintName = "UC_" + uc.Columns[0].ColumnName;
                }

                Type t = pm.GetRootTypeForTable(dt.TableName);
                if ( t == null )
                    continue;

                UniqueConstraintAttribute[] attrs = (UniqueConstraintAttribute[])t.GetCustomAttributes(typeof(UniqueConstraintAttribute), false);
                foreach (UniqueConstraintAttribute a in attrs)
                {
                    List<DataColumn> cols = new List<DataColumn>();
                    foreach ( string col in a.Columns )
                        cols.Add(dt.Columns[col]);
                    dt.Constraints.Add(
                        "UC_" + string.Join("_", a.Columns),
                        cols.ToArray(),
                        false);
                }
            }

            return ds;
        }

        private static void CreateList(PropertyInfo pi, DataTable dt, Polymorpher pm)
        {
            LinkTableAttribute[] linkAttrs = (LinkTableAttribute[])pi.GetCustomAttributes(typeof(LinkTableAttribute), false);
            if ( linkAttrs.Length > 0 )
            {
                CreateLink(dt, linkAttrs[0].Name, linkAttrs[0].ParentColumn, linkAttrs[0].ChildColumn, pi, pm);
            }
            else
            {
                ColumnInfo ci = BuilderUtils.GetColumnInfo(pi);

                if ( ci == null ) // this is many-To-Manu relation with autogenerated link table name
                    CreateLink(dt, null, null, null, pi, pm);
                else
                    CreateRelation(dt.DataSet, dt.TableName, BuilderUtils.GetRelatedTableName(pi, pm), ci);
            }
        }

        private static void CreateLink(DataTable dt, string linkTableName, string parentColName, string childColName, PropertyInfo pi, Polymorpher pm)
        {
            string thisTable = dt.TableName;
            string childTable = BuilderUtils.GetRelatedTableName(pi, pm);
            string thisFK = parentColName ?? thisTable + "ID";
            string childFK = childColName ?? childTable + "ID";

            if ( (childFK == thisFK) || (linkTableName == null && thisTable == childTable) )
                throw new OdxException(
                    "When self-associating table you should use explicit link table name & FK names in link table attribute.");

            if (linkTableName == null)
                linkTableName = thisTable.CompareTo(childTable) > 0 ? thisTable + childTable : childTable + thisTable;

            if (dt.DataSet.Tables.Contains(linkTableName))
                return;

            DataTable link = dt.DataSet.Tables.Add(linkTableName);
            DataColumn linkID = link.Columns.Add("ID", typeof (string));
            link.PrimaryKey = new DataColumn[] { linkID };
            AddProperties(new ColumnInfo(linkID.ColumnName, true, 22), linkID);

            CreateRelation(dt.DataSet, thisTable, link.TableName, new ColumnInfo(thisFK, false, 22));
            CreateRelation(dt.DataSet, childTable, link.TableName, new ColumnInfo(childFK, false, 22));

            link.Constraints.Add(
                "UC_" + thisFK + "_" + childFK,
                new DataColumn[] {link.Columns[thisFK], link.Columns[childFK]},
                false);
        }

        private static void CreateParent(PropertyInfo pi, DataTable dt, Polymorpher pm)
        {
            ColumnInfo ci = BuilderUtils.GetColumnInfo(pi);
            CreateRelation(dt.DataSet, BuilderUtils.GetRelatedTableName(pi, pm), dt.TableName, ci);
        }

        private static void CreateRelation(DataSet ds, string parentTableName, string tableName, ColumnInfo ci)
        {
            string name = "FK_" + parentTableName + "_" + tableName + "_" + ci.Name;
            if ( ds.Relations.Contains(name) )
                return;

            DataTable parent = ds.Tables[parentTableName];
            DataTable child = ds.Tables[tableName];

            DataColumn dc;
            if (child.Columns.Contains(ci.Name))
                dc = child.Columns[ci.Name];
            else
                dc = child.Columns.Add(ci.Name, typeof (string));

            dc.Unique |= ci.IsUnique;

            ds.Relations.Add(name, parent.PrimaryKey[0], dc);

            AddProperties(ci,dc);
        }

        private static void CreateAtom(PropertyInfo pi, DataTable dt)
        {
            Type type = pi.PropertyType;
            if (type.IsEnum)
                type = typeof (int);
            if (type == typeof(Guid))
                type = typeof (string);

            ColumnInfo ci = BuilderUtils.GetColumnInfo(pi);
            DataColumn dc = dt.Columns.Add(ci.Name, type);
            dc.Unique = ci.IsUnique;

            AddProperties(ci, dc);
        }

        private static void AddProperties(ColumnInfo ci, DataColumn dc)
        {
            if ( ci.DbType != null )
                dc.ExtendedProperties["dbType"] = ci.DbType;
            dc.ExtendedProperties["dbMaxLength"] = ci.MaxLength;
            dc.ExtendedProperties["dbScale"] = ci.Scale;
        }
    }
}
